/**
 * Copyright 2011 Honeywell Federal Manufacturing & Technologies, LLC. This material was produced under U.S. Government
 * contract DE-ACO4-01AL66850, Honeywell Federal Manufacturing & Technologies, LLC, (FM&T) with the U. S. Department of
 * Energy for the operation of the Kansas City Plant . The U.S. Government has rights to use, reproduce, and distribute
 * this software.  NEITHER THE GOVERNMENT NOR Honeywell Federal Manufacturing & Technologies, LLC, MAKES ANY WARRANTY,
 * EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE.  If software is modified to produce
 * derivative works, such modified software should be clearly marked, so as not to confuse it with the version available
 * from FM&T.
 *
 * Additionally, this program is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License Version 3 published by the Free Software Foundation. Accordingly, this program is distributed in
 * the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this library.
 * If not, see http://www.gnu.org/licenses/.
 */
package org.braintrain.services.dao;

import com.kcp.ko.fmtnmlib.service.dao.HibernatePersistenceDAOSpringImpl;
import org.braintrain.domain.ContentBase;
import org.braintrain.domain.Module;
import org.braintrain.domain.Navigation;
import org.braintrain.domain.types.ModuleNumbering;
import org.braintrain.services.search.navigation.DeletableUponReleaseQueryFactory;
import org.braintrain.services.search.navigation.StartingNavigationQueryFactory;
import org.braintrain.services.util.ChainListComparator;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;

import java.util.*;


/**
 * The Navigation DAO.  This Class defines the functionality
 * used when dealing with the Navigation.
 * 
 * @author John Ericksen
 *
 */

public class NavigationDAOImpl extends HibernatePersistenceDAOSpringImpl implements NavigationDAO {

    /**
     * Inserts a given navigation between the "current" Navigation and
     * the current.getDeltaNext() navigation
     */
    public Navigation insertNavigationNext(Navigation current, Navigation nav)
    {
        Navigation next = current.getDeltaNext();

        current.setDeltaNext(nav);

        getSession().saveOrUpdate(current);
        getSession().flush();

        nav.setDeltaNext(next);
        //nav.setDeltaPrev(current);

        getSession().saveOrUpdate(nav);
        getSession().flush();

        return nav;
    }

    /**
     * Inserts a given navigation between the "current" Navigation and
     * the current.getDeltaPrev() navigation
     */
    public Navigation insertNavigationPrev(Navigation current, Navigation nav)
    {
        //Navigation prev = current.getDeltaPrev();

        //current.setDeltaPrev(nav);

        getSession().saveOrUpdate(current);
        getSession().flush();

        nav.setDeltaNext(current);
        //nav.setDeltaPrev(prev);

        getSession().saveOrUpdate(nav);
        getSession().flush();

        return nav;
    }

    /**
     * Returns a list of the Main Navigation chain of a given module.  If delta is true,
     * and there exists a getDeltaNext() (See walkChainAndPopulateList()) then that chain is followed
     * instead of the getNext() path.  Also, if include deletes is true, deleted items are added to the
     * list in the appropriate places.
     */
    public List<Navigation> getMainNavigationChain(Module module, boolean delta, boolean includeDeletes) {

        //first regular non-delta page
        Navigation first = (Navigation) this.findUnique(
                new StartingNavigationQueryFactory(module, true, false, includeDeletes));

        if(first == null && delta)
        {
            //first regular delta page
            first = (Navigation) this.findUnique(
                    new StartingNavigationQueryFactory(module, true, true, includeDeletes));
        }

        if(first != null)
        {
            //load all the navigations into the current session
            getSession().createCriteria(Navigation.class)
            .add(Restrictions.eq("module", module))
            .add(Restrictions.eq("mainNavigation", true))
            .addOrder(Order.asc("name").ignoreCase()).list();

            return walkChainAndPopulateList(first, delta, includeDeletes, null);
        }
        return new ArrayList<Navigation>();
    }

    /**
     * Returns a List of Lists of Navigations that represents all the Linked navigation
     * chains for a given module.  This method acts like the getMainNavigationChain but builds up a list
     * of the 'link' chains.
     * 
     */
    public List<List<Navigation>> getLinkNavigationChains(Module module, boolean delta, boolean includeDeletes) {

        List<Navigation> starting = (List<Navigation>)this.find(
                new StartingNavigationQueryFactory(module, false, false, includeDeletes));
        
        //load all the navigations into the current session
        getSession().createCriteria(Navigation.class)
        .add(Restrictions.eq("module", module))
        .add(Restrictions.eq("mainNavigation", false))
        .addOrder(Order.asc("name").ignoreCase()).list();


        List<List<Navigation>> chainList = new ArrayList<List<Navigation>>();
        List<Long> alreadyLinked = new ArrayList<Long>();

        for(Navigation current: starting)
        {
            chainList.add(walkChainAndPopulateList(current, delta, includeDeletes, alreadyLinked));
        }

        if(delta)
        {
            List<Navigation> deltaStarting = (List<Navigation>)this.find(
                    new StartingNavigationQueryFactory(module, false, true, includeDeletes, alreadyLinked));
            for(Navigation current: deltaStarting)
            {
                chainList.add(walkChainAndPopulateList(current, delta, includeDeletes, alreadyLinked));
            }
        }
        Collections.sort(chainList, new ChainListComparator());
        
        return chainList;
    }

    /**
     * Walks a navigation chain starting with "current" and adds to the returned list the items in order.
     *
     * @param delta
     * @param alreadyLinked
     * @return
     */
    private List<Navigation> walkChainAndPopulateList(Navigation start, boolean delta, boolean includeDeletes, List<Long> alreadyLinked)
    {
        List<Navigation> chain = new ArrayList<Navigation>();
        Navigation current = start;
        
        //add preceding deleted items, then backup along the deltaPrevious path to the start of the delta path
        if(delta)
        {
            while(current != null && current.isDeleted()){
                chain.add(current);
                current = current.getNext();
            }

            while(current != null && current.getDeltaPrev() != null){
                current = current.getDeltaPrev();
            }
            //continue
        }

        while(current != null)
        {
            chain.add(current);

            if(delta){
                //add deleted content to the chain
                Navigation deletedContent = current.getNext();
                while(includeDeletes && deletedContent != null && deletedContent.isDeleted()){
                    chain.add(deletedContent);
                    deletedContent = deletedContent.getNext();
                }

                current = current.getDeltaNext();

            }
            else{
                current = current.getNext();
            }

        }

        if(alreadyLinked != null)
        {
            for(Navigation nav: chain)
            {
                alreadyLinked.add(nav.getId());
            }
        }

        return chain;
    }

    /**
     * A simple swap function.  this function swaps the current item with the deltaNext item in the chain (if it exists).
     * EX: (1 -> 2 -> 3 -> 4) -->> (1 -> 3 -> 2 -> 4)
     * 1: currentPrev
     * 2: current
     * 3: destination
     * 4: destinationNext
     */
    public void swapWithNext(Navigation current)
    {
        if(current != null && current.getDeltaNext() != null)
        {
            Navigation destination = current.getDeltaNext();
            Navigation destinationNext = destination.getDeltaNext();
            Navigation currentPrev = current.getDeltaPrev();

            //break links
            if(currentPrev != null)
            {
                currentPrev.setDeltaNext(null);
            }
            current.setDeltaNext(null);

            destination.setDeltaNext(null);
            
            getSession().flush();
            
            //set links
            if(currentPrev != null)
            {
                currentPrev.setDeltaNext(destination);
                getSession().flush();
            }
            
            
            current.setDeltaNext(destinationNext);
            getSession().flush();


            destination.setDeltaNext(current);
            getSession().flush();
            
        }
    }

    /**
     * A more robust swap method.  This method does not assume that the current and destination are in any
     * relation to one another (next-to or separated by 1 or more).
     * 
     * Ex:(1 -> <- 2 -> <- 3)   (4 -> <- 5 -> <- 6) -->> (1 -> <- 5 -> <- 3)   (4 -> <- 2 -> <- 6)
     */
    public void swap(Navigation current, Navigation destination) {
        if(current != null && destination != null && current != destination)
        {
            Navigation destNext = destination.getDeltaNext();
            Navigation destPrev = destination.getDeltaPrev();

            Navigation currNext = current.getDeltaNext();
            Navigation currPrev = current.getDeltaPrev();


            //break links
            /*if(destNext != null)
                destNext.setDeltaPrev(null);*/
            if(destPrev != null)
                destPrev.setDeltaNext(null);

            /*if(currNext != null)
                currNext.setDeltaPrev(null);*/
            if(currPrev != null)
                currPrev.setDeltaNext(null);

            current.setDeltaNext(null);
            //current.setDeltaPrev(null);
            destination.setDeltaNext(null);
            //destination.setDeltaPrev(null);

            //curr and dest are next to eachother
            if(currPrev != null && destNext != null && destination == currPrev && current == destNext)
            {
                current.setDeltaNext(destination);
                //destination.setDeltaPrev(current);
                getSession().flush();
            }
            else if(destPrev != null && currNext != null && current == destPrev && destination == currNext)
            {
                //current.setDeltaPrev(destination);
                destination.setDeltaNext(current);
                getSession().flush();
            }

            //2 -> <- 6
            if(destNext == null || current != destNext)
            {
                current.setDeltaNext(destNext);
                /*if(destNext != null)
                    destNext.setDeltaPrev(current);*/
                getSession().flush();
            }
            //4 -> <- 2
            if(destPrev == null || current != destPrev)
            {
                if(destPrev != null)
                    destPrev.setDeltaNext(current);
                //current.setDeltaPrev(destPrev);
                getSession().flush();
            }

            //1 -> <- 5
            if(currPrev == null || destination != currPrev)
            {
                //destination.setDeltaPrev(currPrev);
                if(currPrev != null)
                    currPrev.setDeltaNext(destination);
                getSession().flush();
            }

            //5 -> <- 3
            if(currNext == null || destination != currNext)
            {
                destination.setDeltaNext(currNext);
                /*if(currNext != null)
                    currNext.setDeltaPrev(destination);*/
                getSession().flush();
            }

            //1 -> <- 5
            if(currPrev == null || destination != currPrev)
            {
                if(currPrev != null)
                    currPrev.setDeltaNext(destination);
                //destination.setDeltaPrev(currPrev);
                getSession().flush();
            }
        }
    }

    /**
     * The core of the release() method.  Saves the delta navigation chain into place and
     * swaps in the delta content.  Also, numbers the pages.
     */
    public void saveDeltaNavigationChanges(Module module)
    {
        Navigation startingNavigation = this.findUnique(new StartingNavigationQueryFactory(module, true, true, false));

        List<Navigation> deleteableNodes = this.find(new DeletableUponReleaseQueryFactory(module));

        for(Navigation deleteNav: deleteableNodes)
        {
            removeReleasedNavigationFromChain(deleteNav);
            ContentBase activeContent = deleteNav.getNullSafeContent().getValue();

            this.delete(deleteNav);

            if(!deleteNav.isTest() && activeContent != null)
            {
                this.delete(activeContent);
            }

        }

        getSession().flush();

        Set<Navigation> allPages = new HashSet<Navigation>(module.getLinkPages());
        allPages.addAll(module.getMainPages());

        //flush out old data
        for(Navigation nav: allPages)
        {
            nav.setNext(null);

            nav.setNavIndex(null);
            nav.setNavIndexOutOf(null);
        }
        getSession().flush();

        //reset links and move pending into released
        for(Navigation nav: allPages)
        {
            nav.setNext(nav.getDeltaNext());

            nav.setDeleted(false);
            nav.setPending(false);
            nav.setMoved(false);

            if(nav.getNullSafeContent().getValue() != null && nav.getNullSafeContent().getDeltaValue()  != null &&
                    nav.getNullSafeContent().getValue().getId().equals(nav.getNullSafeContent().getDeltaValue().getId()) &&
                    !nav.isTest())
            {
                ContentBase deleteBase = nav.getNullSafeContent().getValue();
                nav.getNullSafeContent().setValue(null);
                this.delete(deleteBase);
                getSession().flush();
            }

            if(nav.getNullSafeContent().getDeltaValue() != null)
            {
                nav.getNullSafeContent().setValue(nav.getNullSafeContent().getDeltaValue());
                nav.getNullSafeContent().setDeltaValue(null);
                getSession().flush();
            }
        }
        getSession().flush();

        //number module
        if(module.getNumbering() != ModuleNumbering.None)
        {
            Navigation nav = startingNavigation;
    
            int i = 0;
            boolean restart = false;
            while(nav != null)
            {
                nav.setNavIndex(null);
                nav.setNavIndexOutOf(null);
                
                if(!restart)
                {
                    nav.setNavIndex(++i);
                }
                
                restart = restartNumbering(nav.getNext(), module.getNumbering());
    
                //number backwards outof
                if(restart)
                {
                    Integer outof = i;
                    i = 0;
                    Navigation backNav = nav;
                    while(!restartNumbering(backNav, module.getNumbering()))
                    {
                        backNav.setNavIndexOutOf(outof);
                        backNav = backNav.getPrev();
                    }
                }
                nav = nav.getNext();
            }
        }
    }

    private boolean restartNumbering(Navigation nav, ModuleNumbering numbering) {
        if(nav == null)
        {
            return true;
        }
        switch(numbering)
        {
        case RestartAtBookmarks:
            if(nav.isBookmarkable())
            {
                return true;
            }
            return false;
        case RestartAtTests:
            if(nav.isTest())
            {
                return true;
            }
            return false;
        default:
            return false;
        }
    }

    /**
     * Resets the links around a given navigation (the delta next/previous links).
     */
    public void removeDeltaNavigationFromChain(Navigation nav) {

        Navigation previous = nav.getDeltaPrev();
        Navigation next = nav.getDeltaNext();

        nav.setDeltaNext(null);
        //nav.setDeltaPrev(null);

        nav.setMoved(false);

        getSession().flush();

        if(previous != null){
            previous.setDeltaNext(next);
        }
        /*if(next != null){
            next.setDeltaPrev(previous);
        }*/

        getSession().flush();
    }

    /**
     * resets the links around a given navigation (the delta and regular next/previous links).
     */
    public void removeReleasedNavigationFromChain(Navigation nav) {

        Navigation previous = nav.getPrev();
        Navigation next = nav.getNext();

        nav.setDeltaNext(null);
        //nav.setDeltaPrev(null);
        nav.setNext(null);
        //nav.setPrev(null);

        nav.setMoved(false);

        getSession().flush();

        if(previous != null){
            previous.setNext(next);
        }
        /*if(next != null){
            next.setPrev(previous);
        }*/

        getSession().flush();
    }

    /**
     * Removes all pending deltas from the given module.  Also, deletes
     * any delta content.  In effect, returns the given module to what is currently released
     */
    public void saveSyncDeltaWithReleased(Module module) {

        Set<Navigation> allPages = new HashSet<Navigation>(module.getLinkPages());
        allPages.addAll(module.getMainPages());

        //flush out links
        for(Navigation nav: allPages)
        {
            nav.setDeltaNext(null);
            //nav.setDeltaPrev(null);
        }
        getSession().flush();

        //reset links
        for(Navigation nav: allPages)
        {
            nav.setDeltaNext(nav.getNext());
            //nav.setDeltaPrev(nav.getPrev());
            nav.setDeleted(false);
            nav.setPending(false);
            nav.setMoved(false);

            if(nav.getNullSafeContent().getDeltaValue() != null)
            {
                this.delete(nav.getNullSafeContent().getDeltaValue());
                nav.getNullSafeContent().setDeltaValue(null);
            }
        }
        getSession().flush();
    }
    
}
